// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <zircon/syscalls.h>
#include <zircon/types.h>
#include <fbl/auto_call.h>

#include <dispatcher-pool/dispatcher-event-source.h>
#include <dispatcher-pool/dispatcher-execution-domain.h>
#include <dispatcher-pool/dispatcher-thread-pool.h>

#include <utility>

namespace dispatcher {

EventSource::EventSource(zx_signals_t process_signal_mask)
    : process_signal_mask_(process_signal_mask) {}

EventSource::~EventSource() {
  ZX_DEBUG_ASSERT(domain_ == nullptr);
  ZX_DEBUG_ASSERT(!InExecutionDomain());
  ZX_DEBUG_ASSERT(!InPendingList());
  ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::Idle);
}

void EventSource::InternalDeactivateLocked() {
  // If we are no longer active, we can just get out now.  We should be able
  // to assert that our handle has been closed and that we are in either the
  // Idle or Dispatching state, or that handle is still valid and we are in
  // WaitingOnPort state (meaning that there is a thread in flight from the
  // thread pool which is about to realize that we have become deactivated)
  if (!is_active()) {
    ZX_DEBUG_ASSERT((handle_.is_valid() && (dispatch_state() == DispatchState::WaitingOnPort)) ||
                    (!handle_.is_valid() && ((dispatch_state() == DispatchState::Dispatching) ||
                                             (dispatch_state() == DispatchState::Idle))));
    return;
  }

  // Attempt to cancel any pending operations.  Do not close the handle if it
  // was too late to cancel and we are still waiting on the port.
  CancelPendingLocked();
  if (dispatch_state() != DispatchState::WaitingOnPort) {
    ZX_DEBUG_ASSERT((dispatch_state() == DispatchState::Idle) ||
                    (dispatch_state() == DispatchState::Dispatching));
    handle_.reset();
  }

  // If we still have a domain, remove ourselves from the domain's event
  // source list, then release our reference to it.
  if (domain_ != nullptr) {
    domain_->RemoveEventSource(this);
    domain_ = nullptr;
  }

  // Release our cached thread pool reference.
  thread_pool_.reset();
}

zx_status_t EventSource::ActivateLocked(zx::handle handle, fbl::RefPtr<ExecutionDomain> domain) {
  if ((domain == nullptr) || !handle.is_valid())
    return ZX_ERR_INVALID_ARGS;

  if (is_active() || handle_.is_valid())
    return ZX_ERR_BAD_STATE;
  ZX_DEBUG_ASSERT(thread_pool_ == nullptr);

  auto thread_pool = domain->GetThreadPool();
  if (thread_pool == nullptr)
    return ZX_ERR_BAD_STATE;

  // Add ourselves to our domain's list of event sources.
  zx_status_t res = domain->AddEventSource(fbl::WrapRefPtr(this));
  if (res != ZX_OK)
    return res;

  handle_ = std::move(handle);
  domain_ = std::move(domain);
  thread_pool_ = std::move(thread_pool);

  return ZX_OK;
}

zx_status_t EventSource::WaitOnPortLocked() {
  // If we are attempting to wait, we should not already have a wait pending.
  // In particular, we need to be in the idle state.
  ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::Idle);

  // Attempting to wait when our domain is null indicates that we are in the
  // process of dying, and the wait should be denied.
  if (!is_active())
    return ZX_ERR_BAD_STATE;

  zx_status_t res = DoPortWaitLocked();

  // If the wait async succeeded, then we now have a pending wait operation,
  // and the kernel is now holding an unmanaged reference to us.  Flag the
  // pending wait, and manually bump our ref count.
  if (res == ZX_OK) {
    dispatch_state_ = DispatchState::WaitingOnPort;
    this->AddRef();
  }

  return res;
}

zx_status_t EventSource::CancelPendingLocked() {
  // If we are still active, remove ourselves from the domain's
  // pending work list.
  if (is_active()) {
    // If we were on the pending work list, then our state must have been
    // DispatchPending (and now should be Idle)
    if (domain_->RemovePendingWork(this)) {
      ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::DispatchPending);
      dispatch_state_ = DispatchState::Idle;
    }

    // If there is a wait operation currently pending, attempt to cancel it.
    //
    // If we succeed, manually drop the unmanaged reference which the kernel
    // was holding and transition to the Idle state.
    //
    // If we fail, it must be because the wait has completed and is being
    // dispatched on another thread.  Do not transition to Idle, or release
    // the kernel reference.
    if (dispatch_state() == DispatchState::WaitingOnPort) {
      zx_status_t res = DoPortCancelLocked();

      if (res == ZX_OK) {
        __UNUSED bool should_destruct;

        dispatch_state_ = DispatchState::Idle;
        should_destruct = this->Release();

        ZX_DEBUG_ASSERT(should_destruct == false);
      } else {
        ZX_DEBUG_ASSERT(res == ZX_ERR_NOT_FOUND);
      }
    }
  }

  return (dispatch_state() == DispatchState::Idle) ? ZX_OK : ZX_ERR_BAD_STATE;
}

zx_status_t EventSource::DoPortWaitLocked() {
  ZX_DEBUG_ASSERT(thread_pool_ != nullptr);
  return thread_pool_->WaitOnPort(handle_, reinterpret_cast<uint64_t>(this), process_signal_mask(),
                                  ZX_WAIT_ASYNC_ONCE);
}

zx_status_t EventSource::DoPortCancelLocked() {
  ZX_DEBUG_ASSERT(thread_pool_ != nullptr);
  return thread_pool_->CancelWaitOnPort(handle_, reinterpret_cast<uint64_t>(this));
}

bool EventSource::BeginDispatching() {
  fbl::AutoLock obj_lock(&obj_lock_);
  if (dispatch_state() != DispatchState::DispatchPending)
    return false;

  ZX_DEBUG_ASSERT(InPendingList());

  __UNUSED zx_status_t res;
  res = CancelPendingLocked();
  ZX_DEBUG_ASSERT(res == ZX_OK);
  ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::Idle);

  dispatch_state_ = DispatchState::Dispatching;

  return true;
}

fbl::RefPtr<ExecutionDomain> EventSource::ScheduleDispatch(const zx_port_packet_t& pkt) {
  // Something interesting happened.  Enter the lock and...
  //
  // 1) Sanity check our dispatch state.  We should be in "WaitingOnPort.
  // 2) Assert that something interesting happened.
  // 3) If our domain is still active, add this event source to the pending work
  //    queue.  If we are the first event source to enter the queue, return a
  //    reference to our domain to the dispatcher thread so that it can start
  //    to process the pending work.
  fbl::AutoLock obj_lock(&obj_lock_);

  ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::WaitingOnPort);

  ZX_DEBUG_ASSERT((pkt.type == ZX_PKT_TYPE_INTERRUPT) ||
                  (pkt.signal.observed & process_signal_mask()));

  if (!is_active()) {
    // We have no domain, we must have become deactivated while the thread
    // from the thread pool was in flight (making it impossible for the
    // Deactivator to reclaim the kernel's reference to us).  Go ahead and
    // transition to idle, and close our handle.
    dispatch_state_ = DispatchState::Idle;
    handle_.reset();
    return nullptr;
  }

  // Copy the pending port packet to the internal event source storage,
  // then add ourselves to the domain's pending queue.  If we were the
  // first event source to join our domain's pending queue, then take a
  // reference to our domain so that we can start to process pending work
  // once we have left the obj_lock.  If we are not the first to join the
  // queue, just get out.  The thread which is currently handling pending
  // jobs for this domain will handle this one when the time comes.
  pending_pkt_ = pkt;
  return domain_->AddPendingWork(this) ? domain_ : nullptr;
}

}  // namespace dispatcher
